在本節中,我們將深入探討如何使用Spring Security實現JWT身份驗證,以及如何配置和設置Spring Security來簽發和驗證JWT。此外,我們還將討論如何使用Spring Security來保護您的後端API,以及限制資源的訪問。
首先,我們需要配置Spring Security以使用JWT進行身份驗證。以下是一些示例配置的步驟:
確保您的項目中包含了Spring Security和相關的JWT庫的依賴。您可以在pom.xml
中添加以下依賴:
<!-- Spring Security依賴 3.1.4對應到的核心版本為6.1.4 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
<version>3.1.4</version>
</dependency>
<!-- [Tools]JJwt JWT依賴 -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.11.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.11.5</version>
</dependency>
<!-- ~~以下為其他依賴~~ -->
<!-- [Web]Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- [Tools]Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
在Spring Boot應用程式中,您可以通過創建一個SecurityConfig類來配置Spring Security。以下是一個簡單的配置示例:
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor //Lombok註解,詳細介紹在這個系列的第8天
public class SecruityConfig {
private final JwtAuthenticationFilter jwtAuthFilter; //於"4.2"節實現,此過濾器用於攔截JWT相關請求
private final AuthenticationProvider authenticationProvider;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http, HandlerMappingIntrospector introspector) throws Exception {
MvcRequestMatcher.Builder mvcMatcherBuilder = new MvcRequestMatcher.Builder(introspector).servletPath("/");
http.csrf(csrf->csrf.disable())
.authorizeHttpRequests(auth ->
auth.requestMatchers(
"/error/**",
"/api/register", //用戶註冊
"/api/register/check", //用戶註冊確認
"/api/auth", //用戶登入
"/api/password/forgot", //忘記密碼
"/api/verification/send", //發送驗證碼
"/api/verification/check" //檢查驗證碼
).permitAll()
// 其他路徑需要認證
.anyRequest().authenticated()
)
.sessionManagement(sessionManagement -> {
sessionManagement.sessionCreationPolicy(SessionCreationPolicy.STATELESS); // 無狀態
})
.authenticationProvider(authenticationProvider) // 認證提供者
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
上述配置中使用了JwtAuthenticationFilter
,這是一個自定義的Spring Security配置,用於整合JWT驗證。以下是一個簡單的示例:
@Component
@RequiredArgsConstructor
@Slf4j
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtService jwtService;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(
@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain
) throws ServletException, IOException {
final String authHeader = request.getHeader("Authorization");
final String jwt;
final String userEmail;
// 以下條件為沒有攜帶Token的請求
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
filterChain.doFilter(request, response);
return;
}
jwt = authHeader.substring(7); //從索引7開始取 "Bearer " 後面的Token
userEmail = jwtService.extractUsername(jwt);
if (userEmail != null && SecurityContextHolder.getContext().getAuthentication() == null) {
try {
UserDetails userDetails = this.userDetailsService.loadUserByUsername(userEmail);
if (jwtService.isTokenValid(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
userDetails,
null,
userDetails.getAuthorities()
);
authToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request)
);
SecurityContextHolder.getContext().setAuthentication(authToken);
}else {
response.setStatus(HttpServletResponse.SC_OK);
response.setContentType("application/json");
String errorMessage = "Token has expired";
String jsonErrorMessage = "{\"status\": \"1\", \"message\": \"" + errorMessage + "\"}";
response.getWriter().write(jsonErrorMessage);
return;
}
}catch (ExpiredJwtException ex){
throw ex;
}
}
filterChain.doFilter(request, response);
}
}
配置Spring Security後,我們需要實現JWT的簽發和驗證邏輯,以下為JwtService
的程式碼示例:
@Service
public class JwtService {
// Token有效期限
@Value("${conf.token.expiration}") // 透過文件配置的方式給值,詳細教學在此系列的第7天文章
private Long EXPIRATION_TIME; //單位ms
// 上述兩行可以改寫為下面這行
// private Long EXPIRATION_TIME = 900000L
@Value("${conf.token.secret}") // 透過文件配置的方式給值
private String SECRET_KEY;
// 上述兩行可以改寫為下面這行
// private String SECRET_KEY = "你的私鑰" //在這個範例中我使用的簽名算法為(HS256)"SignatureAlgorithm.HS256",我們可以透過線上的密碼產生器,產生長度64的任意字元組成的字串。注意!如果你使用的是其他算法,則你需要給定該算法對應的私鑰規則,具體可以上網查詢
public String extractUsername(String token) {
try {
return extractClaim(token, Claims::getSubject);
}catch (ExpiredJwtException e){
return e.getClaims().getSubject();
}
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
public String generateToken(UserDetails userDetails) {
return generateToken(new HashMap<>(), userDetails);
}
/**
* 簽發Token
*/
public String generateToken(
Map<String, Object> extractClaims,
UserDetails userDetails
) {
return Jwts
.builder()
.setClaims(extractClaims)
.setSubject(userDetails.getUsername()) //以Username做為Subject
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
.signWith(getSignInKey(), SignatureAlgorithm.HS256)
.compact();
}
/**
* 驗證Token有效性,比對JWT和UserDetails的Username(Email)是否相同
* @return 有效為True,反之False
*/
public boolean isTokenValid(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername())) && !isTokenExpired(token);
}
private boolean isTokenExpired(String token) {
final Date expirationDate = extractExpiration(token);
// return extractExpiration(token).before(new Date());
return expirationDate != null && expirationDate.before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
/**
* 獲取令牌中所有的聲明
* @return 令牌中所有的聲明
*/
private Claims extractAllClaims(String token) {
try {
return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
}
catch (ExpiredJwtException e){
return e.getClaims();
}
}
private Key getSignInKey() {
byte[] keyBytes = Decoders.BASE64.decode(SECRET_KEY);
return Keys.hmacShaKeyFor(keyBytes);
}
}
這就完成了Spring Security和JWT的整合,使您能夠實現安全的身份驗證和授權,並保護您的API資源。
如下圖:
在使用 JWT 進行身份驗證時,探討實現 JWT 刷新令牌的方法至關重要。刷新令牌是一個關鍵的概念,它允許使用者在令牌過期後仍然保持登入狀態,而無需重新輸入使用者名稱和密碼。在這一節中,我們將探討不同的實現方法,以及每種方法的優點和限制。
選擇哪種方法取決於您的應用程序需求和安全性考量。方法一提供了更長時間的登入狀態,但需要額外的伺服器邏輯和刷新令牌的管理。方法二簡化了伺服器端的邏輯,但可能需要用戶在訪問令牌過期時重新登入。根據您的情況,您可以選擇最適合您應用程式的方法。